package com.atguigu.tingshu.common.login;


import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.user.UserInfo;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component
public class GuiguLoginAspect {
    /*
        鉴权的切面 通知方法：使用环绕通知
            为什么？
                1、目标方法执行前鉴权，需要到redis中查询登录状态
                        - 可以将查询到的用户id 使用ThreadLocal存储
                2、放行后 目标方法执行时 可能要用用户信息(userId)
                        - 可以通过ThreadLocal获取用户id
                3、目标方法执行结束，为了防止内存泄露
                        - 可以将ThreadLocal中缓存的数据移除
     */
     @Autowired
     private RedisTemplate redisTemplate;

     //定义了一个切面方法 around，用于拦截带有 @GuiguLogin 注解的方法
     @Around("execution(* com.atguigu.tingshu.*.api.*.*(..))" +
             " && @annotation(guiguLogin)")
     public Object around(ProceedingJoinPoint joinPoint , GuiguLogin guiguLogin) throws Throwable {//guiguLogin表示接收目标方法上的注解对象
         try {
             UserInfo userInfo = null;
             String token = null;
             //1、登录校验
             // 获取请求头中的token: RequestContextHolder会在请求进入时将ServletRequestAttributes对象设置到ThreadLocal中
             ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
                     .getRequestAttributes();
             HttpServletRequest request = requestAttributes.getRequest();
             token = request.getHeader("token");
             //1、如果目标方法需要登录： 先获取token，获取失败抛出异常 否则接收token
             if(guiguLogin.required()){//需要鉴权
                 if(StringUtils.isEmpty(token)){//token不存在 直接抛出异常 让前端重新登录
                     throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
                 }

             }
             //2、token不为空：根据token获取redis中缓存的登录状态
             //            UserInfo
             if(!StringUtils.isEmpty(token)){
                 userInfo = (UserInfo) redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX + token);
             }
             //3、当目标方法用户需要登录 并且 用户信息为空 抛出异常
             //判断登录状态是否有效
             if(null == userInfo && guiguLogin.required()){ //登录状态失效  直接抛出异常 让前端重新登录
                 throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
             }
             //4、如果获取到用户信息 缓存用户信息
             if(null != userInfo){
                 AuthContextHolder.setUserId(userInfo.getId());
                 AuthContextHolder.setUsername(userInfo.getNickname());
             }
             //5、执行目标业务方法
             Object result = joinPoint.proceed();
             //7、返回执行结果
             return result;
         } finally {
             //6、删除缓存
             AuthContextHolder.removeUserId();
             AuthContextHolder.removeUsername();
         }
     }

}